
'use strict';

// var canvas = document.querySelector('canvas');
var localCanvas = document.getElementById('localCanvas');
var remoteCanvas = document.getElementById('remoteCanvas');

var pc1StateDiv = document.querySelector('div#pcState');
var pc1IceStateDiv = document.querySelector('div#pcIceState');

var localSDPSel = 'textarea#localSDP';
var localCandsSel = 'textarea#localCandidates';
var remoteSDPSel = 'textarea#answerSDP';
var remoteCandsSel = 'textarea#answerCandidates';

var localSDPTextarea = document.querySelector(localSDPSel);
var localCandidatesTextarea = document.querySelector(localCandsSel);
var remoteSDPTextarea = document.querySelector(remoteSDPSel);
var remoteCandidatesTextarea = document.querySelector(remoteCandsSel);





var pc_config = {};
pc_config.iceServers=[]; // 
// pc_config.iceServers.push({"url": "stun:121.41.87.159:3478"});
// pc_config.iceTransportPolicy = 'all'; // 'all' , 'relay' , 'none'
pc_config.rtcpMuxPolicy = 'require'; // 'negotiate' , 'require'
pc_config.bundlePolicy = 'max-bundle';


var replace_ip_from = null;
var replace_ip_to = null;
var xswitch_server_url = parseURL('xsw', '/xrtc-switch/');
var is_loopback = parseURL('loopback', 0);
var video_codec = parseURL('vcodec', 'VP8');
var automate = parseURL('auto', 0);
var ptt = parseURL('ptt', 0);
var mixer = parseURL('mixer', null);
var csrc = parseURL('csrc', '100');

// var xswitch_server_url = '/xrtc-switch/';
// var xswitch_server_url = 'https://127.0.0.1:1881/rtc';
// var xswitch_server_url = 'http://121.41.87.159:1880/rtc'; // turn2 

// ping an
// var xswitch_server_url = 'http://video-pa.easemob.com/rtc';
// replace_ip_from = /215.128.35.8/g;
// replace_ip_to = "103.28.214.152";

// var xswitch_server_url = 'http://121.41.87.159:9999/rtc'; // turn2
// replace_ip_from = /121.41.87.159/g;
// replace_ip_to = "127.0.0.1";


console.log('xsw=', xswitch_server_url);
console.log('ip_from=', replace_ip_from);
console.log('ip_to=', replace_ip_to);
console.log('loopback=', is_loopback);
console.log('vcodec=', video_codec);
console.log('auto=', automate);
console.log('ptt=', ptt);
console.log('mixer=', mixer);
console.log('csrc=', csrc);

var localCandidates = new Array();
var remoteCandidates = new Array();

var currentRtcId = null;
var isSentLocalSDP = false;
var pendingLocalCandidates = [];

var isOfferRole = false;
var isSetRemoteSDP = false;
var pendingRemoteCandidates = [];

var startTime;
var localStream = null;
var remoteStream = null;
var localStreamVisualizer = null;
var remoteStreamVisualizer = null;
var pc1;
var pc1Statistics ;
var mediaRecorder = null;
var recordedBlobs = [];

var offerOptions = {
  offerToReceiveAudio: 1,
  offerToReceiveVideo: 1
};

var sdpConstraints = {
  'mandatory': {
    'OfferToReceiveAudio': true,
    'OfferToReceiveVideo': true
  }
};

var localVideo = document.getElementById('localVideo');
var remoteVideo = document.getElementById('remoteVideo');

var joinButton = document.getElementById('joinButton');
if(joinButton){
  joinButton.onclick = function(){
    console.log('click joinButton');
    isSentLocalSDP = false;
    isSetRemoteSDP = false;
    currentRtcId = null;
    isOfferRole = offerRoleCheck.checked;
    console.log('isOfferRole=', isOfferRole);
    joinButton.disabled = true;
    maybeCreatePeerconnection();
    if(isOfferRole){
      openCapturer();  
    }else{
      sendInitC(null);
    }
    
  // createMyOffer();
  };  
}

var exitButton = document.getElementById('exitButton');
if(exitButton){
  exitButton.onclick = function() {
    hangup();
    joinButton.disabled = false;
  };
}

var downloadButton = document.getElementById('downloadButton');
if (downloadButton) {
    downloadButton.onclick = function () {
        var blob = new Blob(recordedBlobs, {type: 'video/webm'});
        var url = window.URL.createObjectURL(blob);
        var a = document.createElement('a');
        a.style.display = 'none';
        a.href = url;
        a.download = 'test.webm';
        document.body.appendChild(a);
        a.click();
        setTimeout(function() {
            document.body.removeChild(a);
            window.URL.revokeObjectURL(url);
        }, 100);
        recordedBlobs = [];
    }
}

var videoInCheck = document.querySelector('input#videoInCheck');
if(videoInCheck){
  videoInCheck.onclick = function() {
    updateVideoEnable();
  };
}

var videoOutCheck = document.querySelector('input#videoOutCheck');
if(videoOutCheck){
  videoOutCheck.onclick = function() {
    updateVideoEnable();
  };
}


var audioInCheck = document.querySelector('input#audioInCheck');
if(audioInCheck){
  audioInCheck.onclick = function() {
    updateAudioInEnable();
  };
}

var audioOutCheck = document.querySelector('input#audioOutCheck');
if(audioOutCheck){
  audioOutCheck.onclick = function() {
    updateAudioOutEnable();
  };
}

var offerRoleCheck = document.querySelector('input#offerRoleCheck');
var loopbackCheck = document.querySelector('input#loopbackCheck');

if(loopbackCheck){
  loopbackCheck.checked = is_loopback == 1?true:false;
  loopbackCheck.onclick = function() {
    if(currentRtcId){
      if(loopbackCheck.checked){
        sendingSubscribeLoopback();
      }else{
        sendingUnsubscribeLoopback();
      }
    }
  };
}


var videoInEnabled = true;
var videoOutEnabled = true;
updateVideoEnable();
function updateVideoEnable() {
  videoInEnabled = videoInCheck.checked;
  videoOutEnabled = videoOutCheck.checked;

  console.log("updateVideoEnable: videoInEnabled=", videoInEnabled);
  console.log("updateVideoEnable: videoOutEnabled=", videoOutEnabled);

  updateVideoInEnable();
  updateVideoOutEnable();

}

function updateVideoInEnable() {
  // localVideo.style=null;
  // if(localVideo){
  //   if(videoInEnabled){
  //     localVideo.style=null;
  //   }else{
  //     localVideo.style='display:none';
  //   }
  // }

  if(localStream){
    localStream.getTracks().forEach(function (track) {
      "video" == track.kind ? track.enabled = videoInEnabled : null
    });
  }
}

function updateVideoOutEnable() {
  // remoteVideo.style=null;
  // if(remoteVideo){
  //   if(videoOutEnabled){
  //     remoteVideo.style=null;
  //   }else{
  //     remoteVideo.style='display:none';
  //   }
  // }

  if(remoteStream){
    remoteStream.getTracks().forEach(function (track) {
      console.log("updateVideoOutEnable: enabled ", videoOutEnabled);
      "video" == track.kind ? track.enabled = videoOutEnabled : null
    });


    remoteVideo.srcObject = null;
    delete remoteVideo.srcObject;
    remoteVideo.srcObject = remoteStream;

    // attachMediaStream(remoteVideo, remoteStream);

  }else{
    if(remoteVideo.srcObject){
      console.log("updateVideoOutEnable: srcObject null");
      remoteVideo.srcObject = null;
      delete remoteVideo.srcObject;
    }
    
  }
}

function updateAudioInEnable() {
  if(localStream && audioInCheck){
    localStream.getTracks().forEach(function (track) {
      "audio" == track.kind ? track.enabled = audioInCheck.checked : null
    });
  }
}

function updateAudioOutEnable() {
  if(remoteStream && audioOutCheck){
    remoteStream.getTracks().forEach(function (track) {
      "audio" == track.kind ? track.enabled = audioOutCheck.checked : null
    });
  }
}






localVideo.addEventListener('loadedmetadata', function() {
  console.log('Local video videoWidth: ' + this.videoWidth +
    'px,  videoHeight: ' + this.videoHeight + 'px');
});

remoteVideo.addEventListener('loadedmetadata', function() {
  console.log('Remote video videoWidth: ' + this.videoWidth +
    'px,  videoHeight: ' + this.videoHeight + 'px');
});

remoteVideo.onresize = function() {
  console.log('Remote video size changed to ' +
    remoteVideo.videoWidth + 'x' + remoteVideo.videoHeight);
  // We'll use the first onsize callback as an indication that video has started
  // playing out.
  if (startTime) {
    var elapsedTime = window.performance.now() - startTime;
    console.log('Setup time: ' + elapsedTime.toFixed(3) + 'ms');
    startTime = null;
  }
};


function openCapturer() {
    console.log('Requesting local stream');
    // switchCaptureButton.disabled = true;
    navigator.mediaDevices.getUserMedia({
      audio: true,
      video: videoInEnabled
    }).then(function (stream) {
      
      // dump local stream
      console.log('got local stream', stream);
      var videoTracks = stream.getVideoTracks();
      var audioTracks = stream.getAudioTracks();
      if (videoTracks) {
        console.log('local videoTracks: ', videoTracks);
      }
      if (audioTracks) {
        console.log('local audioTracks: ', audioTracks);
      }

      localVideo.srcObject = stream;
      // attachMediaStream(localVideo, stream);
      // localVideo.src = URL.createObjectURL(stream);

      localStreamVisualizer = new StreamVisualizer(stream, localCanvas);
      localStreamVisualizer.start();

      localStream = stream;
      // switchCaptureButton.disabled = false;
      // switchCaptureButton.innerHTML =  'Close Capture';

      updateAudioInEnable();

      if(pc1){
        if(isOfferRole){
          createMyOffer();  
        }else{
          createMyAnswer();
        }
        
      }
    })
    .catch(function(e) {
      console.log('openCapturer error: ', e);
      alert('openCapturer error: ' + e.name);
    });
}

function closeCapturer() {
    localStream.getTracks().forEach(function (track) {
                    track.stop();
                });
    // localStream.stop();
    localStream = null;
    switchCaptureButton.innerHTML =  'Open Capture';
    console.log('close local stream');
}

function hangup() {
  console.log('Ending call');

  if (mediaRecorder != null) {
      mediaRecorder.stop();
  }

  if(localStreamVisualizer){
    console.log('stopped localStreamVisualizer');
    localStreamVisualizer.stop();
    localStreamVisualizer = null;
  }

  if(remoteStreamVisualizer){
    console.log('stopped remoteStreamVisualizer');
    remoteStreamVisualizer.stop();
    remoteStreamVisualizer = null;
  }

  if(localStream){
    console.log('stopped localStream');
    localStream.getTracks().forEach(function (track) {
                    track.stop();
                });
    // localStream.stop();
    localStream = null;
  }

  if(remoteStream){
    console.log('stopped remoteStream');
    remoteStream.getTracks().forEach(function (track) {
                    track.stop();
                });

    remoteStream = null;
  }

  if(localVideo.srcObject){
    console.log('stopped localVideo.srcObject');
    localVideo.srcObject = null;
  }

  if(remoteVideo.srcObject){
    console.log('stopped remoteVideo.srcObject');
    remoteVideo.srcObject = null;
  }

  if(pc1){
    if(currentRtcId){
      var msg = {};
      msg.op = 'termC';
      msg.rtcId = currentRtcId;
      $.post( xswitch_server_url, JSON.stringify( msg )
        , function ( data ){
          console.log('post termC success: ', data);
        } );
      currentRtcId = null;
    }

    pc1StateDiv.textContent += ' => ' + pc1.signalingState || pc1.readyState;
    pc1IceStateDiv.textContent += ' => ' + pc1.iceConnectionState;

    pc1.close();
    pc1 = null;
    remoteStream = null;
  }

  cleanStatistics();
  exitButton.disabled = false;
}

function handleRecordedBlobs(event) {
    if (event.data && event.data.size > 0) {
        recordedBlobs.push(event.data);
    }
}

function maybeCreatePeerconnection(){
  if(pc1) {
    return;
  }



  pc1 = new RTCPeerConnection(pc_config);
  pc1Statistics = new WebrtcStatisticsSection();
  console.log('Created local peer connection object pc1');
  
  pc1StateDiv.textContent = pc1.signalingState || pc1.readyState;
  pc1IceStateDiv.textContent = pc1.iceConnectionState;

  pc1.onicecandidate = function(e) {
    onIceCandidate(pc1, e);
  };

  var pc = pc1;
  pc1.oniceconnectionstatechange = function(event) {
    console.log(' ICE state: ' + pc.iceConnectionState);
    // console.log('ICE state change event---: ', event);
    pc1IceStateDiv.textContent += ' => ' + pc.iceConnectionState;
  };

  pc1.onsignalingstatechange = function(){
    var state;
    state = pc.signalingState || pc.readyState;
    console.log('pc1 state change, state: ' + state);
    pc1StateDiv.textContent += ' => ' + state;
  };

  pc1.onaddstream = function (e){
    console.log('received remote stream', e);
    // e.stream.getAudioTracks()[0].enabled = true;
    // e.stream.getVideoTracks()[0] && (event.stream.getVideoTracks()[0].enabled = videoOutEnabled);

    // Call the polyfill wrapper to attach the media stream to this element.
    // attachMediaStream(remoteVideo, e.stream);
    // remoteVideo.srcObject = e.stream;

    remoteStreamVisualizer = new StreamVisualizer(e.stream, remoteCanvas);
    remoteStreamVisualizer.start();
    remoteStream = e.stream;
    updateAudioOutEnable();
    updateVideoOutEnable();


    try {
        mediaRecorder = new MediaRecorder(e.stream, {mimeType: 'video/webm'});
        mediaRecorder.start();
        mediaRecorder.ondataavailable = handleRecordedBlobs;

    } catch (e) {
        console.error('Could not create MediaRecorder: ' + e);
    }

  };

}

function onIceCandidate(pc, event) {
  if (event.candidate) {
    console.log('local ICE candidate:',  event);
    // localCandidatesTextarea.value = event.candidate;
    localCandidates.push(event.candidate);
    localCandidatesTextarea.value = JSON.stringify( localCandidates );
    pendingLocalCandidates.push(event.candidate);
    maybeSendLocalCandidates();
  }
}

function cleanStatistics(){
      timeInfo.innerHTML = '';
      recvInfoDiv.innerHTML =  '';
      sentInfoDiv.innerHTML =  '';
}

function displayStatistics() {
  if(!pc1){
    // console.log('Not start yet');
    return ;
  }
  // if(!remoteStream){
  //   return;
  // }
  //var selector = pc1.getRemoteStreams()[0].getAudioTracks()[0];
  // console.log("getStats ===");
  pc1.getStats(null).then(
    function(results) {
        document.getElementById("stats").innerHTML = dumpStats(results);
      cleanStatistics();
      timeInfo.innerHTML =  new Date() + '<br/>';
      pc1Statistics.parseRecvStatistics(results, function(name, value){
        recvInfoDiv.innerHTML += name + ': ' + value + '<br/>';
      }
      , function(name, value){
        sentInfoDiv.innerHTML += name + ': ' + value + '<br/>';
      });
    }
    , function(e){
      console.log("getStats error: ", e);
    }
  );
  
}



// Display statistics
var timeInfo = document.querySelector('div#timeInfo');
var recvInfoDiv = document.querySelector('div#recvInfo');
var sentInfoDiv = document.querySelector('div#sentInfo');
// var pc1Statistics = new WebrtcStatisticsSection();

var isChrome = navigator.userAgent.indexOf("Chrome") > -1 ;
if(isChrome){
  
}

window.setInterval(displayStatistics, 1000);



// =============== caller part ================

function hoistCodec(typed_sdp, codec_name) {
  if (typeof typed_sdp != 'object' || typeof typed_sdp.sdp != 'string') {
    return typed_sdp;
  }
  var sdp = window.SdpTransform.parse(typed_sdp.sdp);
  if (sdp === undefined || ! Array.isArray(sdp.media)) {
    return typed_sdp;
  }
  for (var media_index in sdp.media) {
    var media = sdp.media[media_index];
    if (typeof media != 'object') {
      continue;
    }
    var type = media.type;
    if (typeof type != 'string') {
      continue;
    }
    if (type !== 'video') {
      continue;
    }
    var rtp = media.rtp;
    if (! Array.isArray(rtp)) {
      continue;
    }
    if (rtp.length > 1) {
        for (var rtp_index in rtp) {
            var rtp_item = rtp[rtp_index];
            if (typeof rtp_item != 'object') {
                continue;
            }
            var codec = rtp_item.codec;
            if (typeof codec != 'string') {
                continue;
            }

            var fmtp;
            for (var fmtp_index in media.fmtp) {
                if (media.fmtp[fmtp_index].payload == rtp_item.payload) {
                    fmtp = media.fmtp[fmtp_index].config;
                }
            }

            if (codec.toUpperCase() === codec_name.toUpperCase()) {
                // rtp.splice(rtp_index, 1);
                // hoisted_rtp.splice(0, 0, rtp_item);
                var hoist = false;
                if (codec_name.toUpperCase() == "H264" && fmtp.search(/profile-level-id=42e01f/) != -1 || codec_name.toUpperCase() == "VP8") {
                    var payloads = window.SdpTransform.parsePayloads(media.payloads);
                    payloads.splice(payloads.indexOf(rtp_item.payload), 1);
                    payloads.splice(0, 0, rtp_item.payload);
                    media.payloads = payloads.join(' ');
                    break;
                }
            }
        }
    }
  }
  typed_sdp.sdp = window.SdpTransform.write(sdp);
  return typed_sdp;
}

function createMyOffer(){
  if(!localStream){
    console.log('can NOT create offer beacause no local stream\n');
    return;
  }

  startTime = window.performance.now();




  maybeCreatePeerconnection();

  console.log('Added local stream to pc1');
  pc1.addStream(localStream);
  
  console.log('pc1 createOffer start');
  pc1.createOffer(offerOptions).then(
  // pc1.createOffer().then(
    function (desc){
      console.log('Offer from pc1\n', desc);
      desc = hoistCodec(desc, video_codec);
      localSDPTextarea.value = JSON.stringify( desc );
      setLocalSdp(desc);
    }
    , function (error){
      console.log('Failed to create session description: ' + error.toString());
    }
  );
}

function createMyAnswer(){
  if(!localStream){
    console.log('can NOT create answer beacause no local stream\n');
    return;
  }

  startTime = window.performance.now();
  var videoTracks = localStream.getVideoTracks();
  var audioTracks = localStream.getAudioTracks();
  if (videoTracks.length > 0) {
    console.log('Using video device: ' + videoTracks[0].label);
  }
  if (audioTracks.length > 0) {
    console.log('Using audio device: ' + audioTracks[0].label);
  }

  console.log('Added local stream to pc1');
  pc1.addStream(localStream);

  pc1.createAnswer(
    function (desc){
      console.log('answer from pc1\n' + desc.sdp);
      desc = hoistCodec(desc, video_codec);
      localSDPTextarea.value = JSON.stringify( desc );
      setLocalSdp(desc);
    }
    , function (error){
      console.log('Failed to create session description: ' + error.toString());
    }
    , sdpConstraints
  );
}



function checkSDPOffer(desc){
  return desc.type === 'offer';
}

function setLocalSdp(desc) {
  console.log('set local sdp ...');
  pc1.setLocalDescription(desc
    , function() {
      console.log('setLocalDescription success');
      if(checkSDPOffer(desc)){
        sendInitC(desc);   
      }else{
        sendAnsC(desc);
      }
    }
    , function(error){
      console.log('Failed to set local description: ' + error.toString());
    });
}

function sendInitC(desc){
      var msg = {};
      msg.op = 'initC';
      if (ptt != 0) {
          msg.ctxId = 'ptt-1';
          msg.confType = 'push-to-talk';
      } else if (mixer) {
          msg.ctxId = mixer;
          msg.confType = 'audio-mixer';
          msg.ctx = 'dummy ctx';
          msg.recCfg = { enabled: true, outDir: "peer-http"};
          msg.csrc = parseInt(csrc);
      }
      msg.tsxId = 'jjkkll-111';
      msg.vcodecs = [video_codec];
      if(desc){
        msg.sdp = desc;
      }
      // msg.path = 'rrr';
      // msg.fname = 'a';
      
      console.log('sending initC');
      $.post( xswitch_server_url, JSON.stringify( msg )
        , function ( data ){
          if(desc){
            afterSendinngOffer(data);
          }else{
            afterGotRemoteOffer(data);
          }
          if(loopbackCheck.checked){
            sendingSubscribeLoopback();
          }
        } );
}

function sendAnsC(desc){
      var msg = {};
      msg.rtcId = currentRtcId;
      msg.op = 'ansC';
      if(desc){
        msg.sdp = desc;  
      }
      
      console.log('sending ansC');
      $.post( xswitch_server_url, JSON.stringify( msg )
        , function ( data ){
          isSentLocalSDP = true;
          setRemoteJson(data);
          maybeSendLocalCandidates();
        } );
}


function afterSendinngOffer(data){
  console.log('post offer success: ', data);
  isSentLocalSDP = true;
  if(data.rtcId){
    currentRtcId = data.rtcId;
    console.log('update rtcId: ', currentRtcId);
  }
  setRemoteJson(data);
  maybeSetRemoteCandidates();
  maybeSendLocalCandidates();
}

function afterGotRemoteOffer(data){
  if(data.rtcId){
    currentRtcId = data.rtcId;
    console.log('update rtcId: ', currentRtcId);
  }
  setRemoteJson(data);
}

function sendingSubscribeLoopback(){
      var msg = {};
      msg.op = 'subC';
      msg.rtcId = currentRtcId;
      msg.pubRtcId = currentRtcId;
      msg.audio = true;
      msg.video = true; 
      console.log('sending subC', msg);
      $.post( xswitch_server_url, JSON.stringify( msg )
        , function ( data ){
          console.log('subC response: ', data);
        } );
}

function sendingUnsubscribeLoopback(){
      var msg = {};
      msg.op = 'usubC';
      msg.rtcId = currentRtcId;
      msg.pubRtcId = currentRtcId;
      
      console.log('sending usubC', msg);
      $.post( xswitch_server_url, JSON.stringify( msg )
        , function ( data ){
          console.log('usubC response: ', data);
        } );
}


function maybeSendLocalCandidates(){
  if(!currentRtcId || !isSentLocalSDP){
    return ;
  }

  var candidates = pendingLocalCandidates;
  pendingLocalCandidates = [];

  console.log('sending candidates: ', candidates);

  var msg = {};
  msg.op = 'tcklC';
  msg.rtcId = currentRtcId;
  msg.candidates = candidates;
  $.post( xswitch_server_url, JSON.stringify( msg )
    , function ( data ){
      console.log('post candidates success: ', data);
    } );

}


function setRemoteJson(data){
  
  if(data.candidates){
    data.candidates.forEach(function(e){
      pendingRemoteCandidates.push(e);
    });
  }

  if(data.sdp){
    var desc = new RTCSessionDescription(data.sdp);
    console.log('setRemoteDescription ', desc);
    pc1.setRemoteDescription(desc
      , function() {
        console.log('setRemoteDescription success');
        isSetRemoteSDP = true;
        maybeSetRemoteCandidates();

        if(checkSDPOffer(desc)){
          openCapturer();
        }
      }
      , function(error){
        console.log('Failed to set remote description: ' + error.toString());
      });
  }

  maybeSetRemoteCandidates();
}

function maybeSetRemoteCandidates(){
  if(!isSetRemoteSDP || !isSentLocalSDP){
    return ;
  }

  var candidates = pendingRemoteCandidates;
  pendingRemoteCandidates = [];
  candidates.forEach(function(e){
    if(replace_ip_from){
      //e.candidate = e.candidate.replace(/215.128.35.8/g, "103.28.214.152");
      e.candidate = e.candidate.replace(replace_ip_from, replace_ip_to);      
    }

    var c = new RTCIceCandidate(e);
    console.log('add remote candidate', c);
    pc1.addIceCandidate(c); 
  });
}


function parseURL(name, defaultValue){  
  var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)","i");  
  var r = window.location.search.substr(1).match(reg);  
  if(r!=null){
    return unescape(r[2]);
  } 
  return defaultValue;  
}

function toggle_call() {
  var interval = 10000;
  if (!joinButton.disabled) {
    joinButton.click();
    interval = 10000;
  } else if (!exitButton.disabled) {
    exitButton.click();
    interval = 1000;
  }
  window.setTimeout(toggle_call, interval);
}

if (automate != 0) {
  window.setTimeout(toggle_call, 1000);
}

function dumpStats(results) {
    var statsString = '';
    results.forEach(function(res) {
        statsString += '<h3>Report type=';
        statsString += res.type;
        statsString += '</h3>\n';
        statsString += 'id ' + res.id + '<br>\n';
        statsString += 'time ' + res.timestamp + '<br>\n';
        Object.keys(res).forEach(function(k) {
            if (k !== 'timestamp' && k !== 'type' && k !== 'id') {
                statsString += k + ': ' + res[k] + '<br>\n';
            }
        });
    });
    return statsString;
}
